package com.mikhaellopez.circularfillableloaders;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Point;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.media.image.PixelMap;

/**
 * Created by Mikhael LOPEZ on 09/10/2015.
 * Copyright (C) 2017 Mikhael LOPEZ
 * Licensed under the Apache License Version 2.0
 */
public class CircularFillableLoaders extends Component {
    // Default values
    private static final float DEFAULT_AMPLITUDE_RATIO = 0.05f;
    private static final float DEFAULT_WATER_LEVEL_RATIO = 0.4f;
    private static final float DEFAULT_WAVE_LENGTH_RATIO = 1.0f;
    private static final float DEFAULT_WAVE_SHIFT_RATIO = 0.0f;
    public static final int DEFAULT_WAVE_COLOR = Color.BLACK.getValue();
    public static final int DEFAULT_BORDER_WIDTH = 10;

    // Dynamic Properties
    private int canvasSize;
    private float amplitudeRatio;
    private int waveColor;

    // Properties
    private float waterLevelRatio = DEFAULT_WATER_LEVEL_RATIO;
    private float waveShiftRatio = DEFAULT_WAVE_SHIFT_RATIO;
    private float defaultWaterLevel;

    // Object used to draw
    private PixelMap image;
    private Element drawable;
    private Paint paint;
    private Paint borderPaint;
    private Paint wavePaint;
    private PixelMapShader waveShader;
    private Matrix waveShaderMatrix;
    private float borderWidth;

    // Animation
    private AnimatorValue animatorSetWave;

    private boolean firstLoadBitmap = true;

    // region Constructor & Init Method
    public CircularFillableLoaders(final Context context) {
        this(context, null);
    }

    public CircularFillableLoaders(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public CircularFillableLoaders(Context context, AttrSet attrs, String defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
        onDraw();
    }

    private void init(Context context, AttrSet attrs, String defStyleAttr) {
        // Init paint
        paint = new Paint();
        paint.setAntiAlias(true);

        // Init Wave
        waveShaderMatrix = new Matrix();
        wavePaint = new Paint();
        wavePaint.setAntiAlias(true);
        wavePaint.setStrokeWidth(2);

        // Init Border
        borderPaint = new Paint();
        borderPaint.setAntiAlias(true);
        borderPaint.setStyle(Paint.Style.STROKE_STYLE);

        // Init Animation
        initAnimation();

        // Load the styled attributes and set their properties
        // Init Wave

        drawable = AttrUtils.getElementFromAttr(attrs, "cfl_img", null);
        waveColor = AttrUtils.getColorFromAttr(attrs, "cfl_wave_color", DEFAULT_WAVE_COLOR);
        float amplitudeRatioAttr = AttrUtils.getFloatFromAttr(attrs, "cfl_wave_amplitude", DEFAULT_AMPLITUDE_RATIO);
        amplitudeRatio = (amplitudeRatioAttr > DEFAULT_AMPLITUDE_RATIO) ? DEFAULT_AMPLITUDE_RATIO : amplitudeRatioAttr;
        setProgress(AttrUtils.getIntFromAttr(attrs, "cfl_progress", 0));

        if (AttrUtils.getBooleanFromAttr(attrs, "cfl_border", true)) {
            borderWidth = AttrUtils.getDimensionFromAttr(attrs, "cfl_border_width", DEFAULT_BORDER_WIDTH * getResourceManager().getDeviceCapability().screenDensity / 160);
        } else {
            borderWidth = 0;
        }
        if (drawable != null) {
            image = drawableToBitmap(drawable);
        }
    }
    // endregion

    // region Draw Method
    private void onDraw() {
        // Load the bitmap
        addDrawTask(new DrawTask() {
            @Override
            public void onDraw(Component component, Canvas canvas) {
                if (image == null) {
                    return;
                }
                canvasSize = getWidth();
                if (getHeight() < canvasSize) {
                    canvasSize = getHeight();
                    canvas.translate((getWidth() - getHeight()) * 0.5f, 0);
                } else {
                    canvas.translate(0, (getHeight() - getWidth()) * 0.5f);
                }
                int circleCenter = canvasSize / 2;
                canvas.drawCircle(circleCenter, circleCenter, circleCenter - borderPaint.getStrokeWidth(), paint);


                canvas.drawPixelMapHolderCircleShape(new PixelMapHolder(image), new RectFloat(0, 0, image.getImageInfo().size.width, image.getImageInfo().size.height),
                        circleCenter, circleCenter, circleCenter);
                updateWaveShader(canvas);

                borderPaint.setColor(new Color(waveColor));
                if (borderWidth > 0) {
                    if (borderWidth > canvasSize / 2f) {
                        borderPaint.setStrokeWidth(canvasSize / 2f);
                        canvas.drawCircle(canvasSize / 2f, canvasSize / 2f, canvasSize / 4f, borderPaint);
                    } else {
                        borderPaint.setStrokeWidth(borderWidth);
                        canvas.drawCircle(canvasSize / 2f, canvasSize / 2f, (canvasSize - borderWidth) / 2f, borderPaint);
                    }
                }

            }
        });
    }

    public void setPixelMap(PixelMap pixelMap) {
        image = pixelMap;
        invalidate();
    }

    public void setPixelMap(int resId) {
        image = ResUtils.decodeResource(getContext(), resId);
        invalidate();
    }

    public void setPixelMap(Element element) {
        drawable = element;
        if (drawable != null) {
            image = drawableToBitmap(drawable);
            invalidate();
        }
    }

    private void updateWaveShader(Canvas canvas) {
        float radius = canvasSize / 2;

        double defaultAngularFrequency = 2.0f * Math.PI / DEFAULT_WAVE_LENGTH_RATIO / canvasSize;
        float defaultAmplitude = canvasSize * amplitudeRatio;
        defaultWaterLevel = canvasSize * waterLevelRatio;

        wavePaint.setColor(new Color(adjustAlpha(waveColor, 0.3f)));
        for (int beginX = 0; beginX < canvasSize; beginX++) {
            double wx = beginX * defaultAngularFrequency;
            float x = beginX + canvasSize * waveShiftRatio > canvasSize ? beginX + canvasSize * waveShiftRatio - canvasSize : beginX + canvasSize * waveShiftRatio;
            float sqrt = (float) Math.sqrt(radius * radius - Math.abs(x - radius) * Math.abs(x - radius));
            float yt = radius - sqrt;
            float yb = sqrt + radius;
            float beginY = Math.max((float) (defaultWaterLevel + defaultAmplitude * Math.sin(wx)), yt);
            if (yb > beginY) {
                canvas.drawLine(new Point(x, beginY), new Point(x, yb), wavePaint);
            }
        }

        wavePaint.setColor(new Color(waveColor));
        float offsetwaveShiftRatio = waveShiftRatio - 0.25f;
        if (offsetwaveShiftRatio < 0) {
            offsetwaveShiftRatio += 1;
        }
        for (int beginX = 0; beginX < canvasSize; beginX++) {
            double wx = beginX * defaultAngularFrequency;
            float x = beginX + canvasSize * offsetwaveShiftRatio > canvasSize ? beginX + canvasSize * offsetwaveShiftRatio - canvasSize : beginX + canvasSize * offsetwaveShiftRatio;
            float sqrt = (float) Math.sqrt(radius * radius - Math.abs(x - radius) * Math.abs(x - radius));
            float yt = radius - sqrt;
            float yb = sqrt + radius;
            float beginY = Math.max((float) (defaultWaterLevel + defaultAmplitude * Math.sin(wx)), yt);
            if (yb > beginY) {
                canvas.drawLine(new Point(x, beginY), new Point(x, yb), wavePaint);
            }
        }
    }

    private PixelMap drawableToBitmap(Element drawable) {
        if (drawable instanceof PixelMapElement) {
            return ((PixelMapElement) drawable).getPixelMap();
        }
        return null;
    }
    // endregion

    private int measureWidth(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.PRECISE) {
            // The parent has determined an exact size for the child.
            result = specSize;
        } else if (specMode == MeasureSpec.NOT_EXCEED) {
            // The child can be as large as it wants up to the specified size.
            result = specSize;
        } else {
            // The parent has not imposed any constraint on the child.
            result = getWidth();
        }
        return result;
    }

    private int measureHeight(int measureSpecHeight) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpecHeight);
        int specSize = MeasureSpec.getSize(measureSpecHeight);

        if (specMode == MeasureSpec.PRECISE) {
            // We were told how big to be
            result = specSize;
        } else if (specMode == MeasureSpec.NOT_EXCEED) {
            // The child can be as large as it wants up to the specified size.
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = getWidth();
        }
        return (result + 2);
    }
    // endregion

    // region Set Attr Method
    public void setColor(int color) {
        waveColor = color;
        invalidate();
    }

    public void setBorderWidth(float width) {
        borderWidth = width;
        invalidate();
    }

    public void setAmplitudeRatio(float amplitudeRatio) {
        if (this.amplitudeRatio != amplitudeRatio) {
            this.amplitudeRatio = amplitudeRatio;
            invalidate();
        }
    }

    public void setProgress(int progress) {
        setProgress(progress, 1000);
    }


    AnimatorValue animatorValue = new AnimatorValue();
    public void setProgress(int progress, int milliseconds) {
        // vertical animation.
        if (animatorValue.isRunning()) animatorValue.stop();
        animatorValue.setCurveType(Animator.CurveType.DECELERATE);
        animatorValue.setDuration(milliseconds);
        float start = waterLevelRatio;
        float end = 1f - ((float) progress / 100);
        animatorValue.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float v) {
                waterLevelRatio = start + (end - start) * v;
            }
        });
        animatorValue.start();
    }
    // endregion

    // region Animation
    private void startAnimation() {
        if (animatorSetWave != null) {
            animatorSetWave.start();
        }
    }

    private void initAnimation() {
        // horizontal animation.
        animatorSetWave = new AnimatorValue();
        animatorSetWave.setLoopedCount(AnimatorValue.INFINITE);
        animatorSetWave.setDuration(1000);
        animatorSetWave.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float v) {
                waveShiftRatio = v;
                invalidate();
            }
        });
        animatorSetWave.start();

    }

    private void setWaveShiftRatio(float waveShiftRatio) {
        if (this.waveShiftRatio != waveShiftRatio) {
            this.waveShiftRatio = waveShiftRatio;
            invalidate();
        }
    }

    private void setWaterLevelRatio(float waterLevelRatio) {
        if (this.waterLevelRatio != waterLevelRatio) {
            this.waterLevelRatio = waterLevelRatio;
            invalidate();
        }
    }

    private void cancel() {
        if (animatorSetWave != null) {
            animatorSetWave.end();
        }
    }


    private int adjustAlpha(int color, float factor) {
        int alpha = Math.round(Color.alpha(color) * factor);
        int red = RgbColor.fromArgbInt(color).getRed();
        int green = RgbColor.fromArgbInt(color).getGreen();
        int blue = RgbColor.fromArgbInt(color).getBlue();
        return Color.argb(alpha, red, green, blue);
    }
}
